Sending Object State Notifications using Delegates

Clearly, the previous SimpleDelegate example was intended to be purely illustrative in nature, given that there would be no compelling reason to define a delegate simply to add two numbers! To provide a more realistic use of delegate types, let’s use delegates to define a Car class that has the ability to inform external entities about its current engine state. To do so, we will take the following steps:

To begin, create a new Console Application project named CarDelegate. Now, define a new Car class that looks initially like this:

public class Car
{
    // Internal state data.
    public int CurrentSpeed { get; set; }
    public int MaxSpeed { get; set; }
    public string PetName { get; set; }

    // Is the car alive or dead?
    private bool carIsDead;
    
    // Class constructors.
    public Car() { MaxSpeed = 100; }
    public Car(string name, int maxSp, int currSp)
    {
        CurrentSpeed = currSp;
        MaxSpeed = maxSp;
        PetName = name;
    }
}

Now, consider the following updates, which address the first three points:

public class Car
{
...
    // 1) Define a delegate type.    
    public delegate void CarEngineHandler(string msgForCaller);
    
    // 2) Define a member variable of this delegate.
    private CarEngineHandler listOfHandlers;

    // 3) Add registration function for the caller.
    public void RegisterWithCarEngine(CarEngineHandler methodToCall)
    {
        listOfHandlers = methodToCall;
    }
}

Notice in this example that we define the delegate types directly within the scope of the Car class. As you explore the base class libraries, you will find it is quite common to define a delegate within the scope of the type it naturally works with. Our delegate type, CarEngineHandler, can point to any method taking a single string as input and void as a return value.

Next, note that we declare a private member variable of our delegate (named listOfHandlers), and a helper function (named RegisterWithCarEngine()) that allows the caller to assign a method to the delegate’s invocation list.

Note Strictly speaking, we could have defined our delegate member variable as public, therefore avoiding the need to create additional registration methods. However, by defining the delegate member variable as private, we are enforcing encapsulation services and providing a more type-safe solution. You’ll revisit the risk of public delegate member variables later in this chapter when you look at the C# event keyword.

At this point, we need to create the Accelerate() method. Recall, the point here is to allow a Car object to send engine-related messages to any subscribed listener. Here is the update:

public void Accelerate(int delta)
{
    // If this car is 'dead', send dead message.
    if (carIsDead)
    {
        if (listOfHandlers != null)
            listOfHandlers("Sorry, this car is dead...");
    }
    else
    {
        CurrentSpeed += delta;

        // Is this car 'almost dead'?
        if (10 == (MaxSpeed - CurrentSpeed)
            && listOfHandlers != null)
        {
            listOfHandlers("Careful buddy! Gonna blow!");
        }
        if (CurrentSpeed >= MaxSpeed)
            carIsDead = true;
        else
            Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed);
    }
}

Notice that before we invoke the methods maintained by the listOfHandlers member variable, we are checking it against a null value. The reason is that it will be the job of the caller to allocate these objects by calling the RegisterWithCarEngine() helper method. If the caller does not call this method and we attempt to invoke the delegate’s invocation list, we will trigger a NullReferenceException and bomb at runtime—which would obviously be a bad thing! Now that we have the delegate infrastructure in place, observe the updates to the Program class:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("***** Delegates as event enablers *****\n");

        // First, make a Car object.
        Car c1 = new Car("SlugBug", 100, 10);

        // Now, tell the car which method to call
        // when it wants to send us messages.
        c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));

        // Speed up (this will trigger the events).
        Console.WriteLine("***** Speeding up *****");
        for (int i = 0; i < 6; i++)
            c1.Accelerate(20);
        Console.ReadLine();
    }

    // This is the target for incoming events.
    public static void OnCarEngineEvent(string msg)
    {
        Console.WriteLine("\n***** Message From Car Object *****");
        Console.WriteLine("=> {0}", msg);
        Console.WriteLine("***********************************\n");
    }
}

The Main() method begins by simply making a new Car object. Since we are interested in hearing about the engine events, our next step is to call our custom registration function, RegisterWithCarEngine(). Recall that this method expects to be passed an instance of the nested CarEngineHandler delegate, and as with any delegate, we specify a “method to point to” as a constructor parameter.

The trick in this example is that the method in question is located back in the Program class! Again, notice that the OnCarEngineEvent() method is a dead-on match to the related delegate in that it takes a string as input and returns void. Consider the output of the current example:

***** Delegates as event enablers *****

***** Speeding up *****
CurrentSpeed = 30
CurrentSpeed = 50
CurrentSpeed = 70

***** Message From Car Object *****
=> Careful buddy! Gonna blow!
***********************************

CurrentSpeed = 90

***** Message From Car Object *****
=> Sorry, this car is dead...
***********************************

Enabling Multicasting

Recall that .NET delegates have the built-in ability to multicast. In other words, a delegate object can maintain a list of methods to call, rather than just a single method. When you wish to add multiple methods to a delegate object, you simply make use of the overloaded += operator, rather than a direct assignment. To enable multicasting on the Car class, we could update the RegisterWithCarEngine()method like so:

public class Car
{
    // Now with multicasting support!
    // Note we are now using the += operator, not
    // the assignment operator (=).
    public void RegisterWithCarEngine(CarEngineHandler methodToCall)
    {
        listOfHandlers += methodToCall;
    }
...
}

With this simple change, the caller can now register multiple targets for the same callback notification. Here, our second handler prints the incoming message in uppercase, just for display purposes:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("***** Delegates as event enablers *****\n");

        // First, make a Car object.
        Car c1 = new Car("SlugBug", 100, 10);

        // Register multiple targets for the notifications.
        c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
        c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent2));

        // Speed up (this will trigger the events).
        Console.WriteLine("***** Speeding up *****");
        for (int i = 0; i < 6; i++)
            c1.Accelerate(20);
        Console.ReadLine();
    }

    // We now have TWO methods that will be called by the Car
    // when sending notifications.
    public static void OnCarEngineEvent(string msg)
    {
        Console.WriteLine("\n***** Message From Car Object *****");
        Console.WriteLine("=> {0}", msg);
        Console.WriteLine("***********************************\n");
    }

    public static void OnCarEngineEvent2(string msg)
    {
        Console.WriteLine("=> {0}", msg.ToUpper());
    }
}

In terms of CIL code, the += operator resolves to a call to the static Delegate.Combine() method (in fact, you could call Delegate.Combine() directly, but the += operator offers a simpler alternative). Ponder the following CIL implementation of RegisterWithCarEngine():

.method public hidebysig instance void RegisterWithCarEngine(
 class CarDelegate.Car/CarEngineHandler methodToCall) cil managed
{
 // Code size 25 (0x19)
 .maxstack 8
 IL_0000: nop
 IL_0001: ldarg.0
 IL_0002: dup
 IL_0003: ldfld class CarDelegate.Car/CarEngineHandler CarDelegate.Car::listOfHandlers
 IL_0008: ldarg.1
 
 IL_0009: call class [mscorlib]System.Delegate [mscorlib]
  System.Delegate::Combine(class [mscorlib]System.Delegate,
  class [mscorlib]System.Delegate)

 IL_000e: castclass CarDelegate.Car/CarEngineHandler
 IL_0013: stfld class CarDelegate.Car/CarEngineHandler CarDelegate.Car::listOfHandlers
 IL_0018: ret
} // end of method Car::RegisterWithCarEngine

Removing Targets from a Delegate’s Invocation List

The Delegate class also defines a static Remove() method that allows a caller to dynamically remove a method from a delegate object’s invocation list. This makes it simple to allow the caller to “unsubscribe” from a given notification at runtime. While you could call Delegate.Remove() directly in code, C# developers can use the -= operator as a convenient shorthand notation.

Let’s add a new method to the Car class that allows a caller to remove a method from the invocation list:

public class Car
{
...
    public void UnRegisterWithCarEngine(CarEngineHandler methodToCall)
    {
        listOfHandlers -= methodToCall;
    }
}

Again, the -= syntax is simply a shorthand notation for manually calling the static Delegate.Remove() method, as illustrated by the following CIL code for the UnRegisterWithCarEvent() method of the Car class:

.method public hidebysig instance void UnRegisterWithCarEngine(
class CarDelegate.Car/CarEngineHandler methodToCall) cil managed
{
 // Code size 25 (0x19)
 .maxstack 8
 IL_0000: nop
 IL_0001: ldarg.0
 IL_0002: dup
 IL_0003: ldfld class CarDelegate.Car/CarEngineHandler CarDelegate.Car::listOfHandlers
 IL_0008: ldarg.1

 IL_0009: call class [mscorlib]System.Delegate [mscorlib]
  System.Delegate::Remove(class [mscorlib]System.Delegate,
  class [mscorlib]System.Delegate)

 IL_000e: castclass CarDelegate.Car/CarEngineHandler
 IL_0013: stfld class CarDelegate.Car/CarEngineHandler CarDelegate.Car::listOfHandlers
 IL_0018: ret
} // end of method Car::UnRegisterWithCarEngine

With the current updates to the Car class, we could stop receiving the engine notification on the second handler by updating Main() as follows:

static void Main(string[] args)
{
    Console.WriteLine("***** Delegates as event enablers *****\n");

    // First, make a Car object.
    Car c1 = new Car("SlugBug", 100, 10);
    c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));

    // This time, hold onto the delegate object,
    // so we can unregister later.
    Car.CarEngineHandler handler2 = new Car.CarEngineHandler(OnCarEngineEvent2);
    c1.RegisterWithCarEngine(handler2);

    // Speed up (this will trigger the events).
    Console.WriteLine("***** Speeding up *****");
    for (int i = 0; i < 6; i++)
        c1.Accelerate(20);

    // Unregister from the second handler.
    c1.UnRegisterWithCarEngine(handler2);

    // We won't see the 'uppercase' message anymore!
    Console.WriteLine("***** Speeding up *****");
    for (int i = 0; i < 6; i++)
        c1.Accelerate(20);
    
    Console.ReadLine();
}

One difference in Main() is that this time we are creating a Car.CarEngineHandler object and storing it in a local variable so we can use this object to unregister with the notification later on. Thus, the second time we speed up the Car object, we no longer see the uppercase version of the incoming message data, as we have removed this target from the delegates invocation list.

Source Code The CarDelegate project is located under the Chapter 11 subdirectory.